import asyncio
import bisect
import random
from datetime import datetime

from . import logger, util
from .constants import ValueType
from .encodings import ValueRepresentation
from .util import get_next_update_interval

"""
Update interval between update runs (in seconds).
"""
DEFAULT_UPDATE_FREQUENCY = 5

"""
Interval between reinit runs (in seconds).
"""
DEFAULT_REINIT_RATE = 60

"""
Disable dynamic frequency by default
"""
DEFAULT_ENABLE_DYNAMIC_FREQUENCY = False


class MIBUpdater:
    """
    Interface for developing OID handlers that require persistent (or background) execution.
    """

    def __init__(self):
        self.run_event = asyncio.Event()
        self.frequency = DEFAULT_UPDATE_FREQUENCY
        self.enable_dynamic_frequency = DEFAULT_ENABLE_DYNAMIC_FREQUENCY
        self.reinit_rate = DEFAULT_REINIT_RATE // DEFAULT_UPDATE_FREQUENCY
        self.update_counter = self.reinit_rate + 1  # reinit_data when init

    async def start(self):
        # Run the update while we are allowed
        redis_exception_happen = False
        while self.run_event.is_set():
            start = datetime.now()
            try:
                # reinit internal structures
                if self.update_counter > self.reinit_rate:
                    # reconnect when redis exception happen
                    if redis_exception_happen:
                        self.reinit_connection()

                    self.reinit_data()
                    self.update_counter = 0
                else:
                    self.update_counter += 1

                # run the background update task
                self.update_data()
                redis_exception_happen = False
            except RuntimeError:
                # Any unexpected exception or error, log it and keep running
                logger.exception("MIBUpdater.start() caught a RuntimeError during update_data(), will reinitialize the connections")
                # When redis server restart, swsscommon will throw swsscommon.RedisError, redis connection need re-initialize in reinit_data()
                # TODO: change to swsscommon.RedisError
                redis_exception_happen = True
            except Exception:
                # Any unexpected exception or error, log it and keep running
                logger.exception("MIBUpdater.start() caught an unexpected exception during update_data()")

            if self.enable_dynamic_frequency:
                """
                On SONiC device with huge interfaces
                  for example RP card on ethernet chassis, including backend ports, 600+ interfaces
                The update_data function could be very slow, especially when 100% CPU utilization.
                  for example ciscoSwitchQosMIB.QueueStatUpdater, uses 1-3 seconds on normal state.
                                                                  uses 3-8 seconds on 100% CPU utilization state.
                We use Asyncio/Coroutine as the basic framework,
                  the mib updaters share the same asyncio event loop with the SNMP agent client.
                  Hence during the updaters executing, the agent client can't receive/respond to new requests,

                The high frequency and the long execution time
                  causes the SNMP request to be timed out on High CPU utilization.
                The stable frequency(generally with default value 5s)
                  doesn't works well on this huge interfaces situation.
                when the execution time is long,
                  wait for longer time to give back the control of asyncio event loop to SNMP agent
                """
                execution_time = (datetime.now() - start).total_seconds()
                next_frequency = get_next_update_interval(execution_time, self.frequency)

                if next_frequency > self.frequency:
                    logger.debug(f"MIBUpdater type[{type(self)}] slow update detected, "
                                 f"update execution time[{execution_time}], next_frequency[{next_frequency}]")
            else:
                next_frequency = self.frequency

            # wait based on our update frequency before executing again.
            # randomize to avoid concurrent update storms.
            await asyncio.sleep(next_frequency + random.randint(-2, 2))

    def reinit_data(self):
        """
        Reinit task. Children may override this method.
        """
        return

    def reinit_connection(self):
        """
        Reinit redis connection task. Children may override this method.
        """
        return

    def update_data(self):
        """
        Background task. Children must override this method.
        """
        raise NotImplementedError()


class MIBMeta(type):
    KEYSTORE = '__subids__'
    PREFIXES = '__subtrees__'
    UPDATERS = '__updaters__'

    def __new__(mcs, name, bases, attributes, prefix=None):
        cls = type.__new__(mcs, name, bases, attributes)

        # each object-type, ie. MIBEntry will has a prefix
        # ref: https://tools.ietf.org/html/rfc2741#section-2.1
        prefixes = []

        if prefix is not None:
            if not util.is_valid_oid(prefix, dot_prefix=True):
                raise ValueError("Invalid prefix '{}' for class '{}'".format(prefix, name))

            _prefix = util.oid2tuple(prefix)
            _prefix_len = len(_prefix)
            for me in vars(cls).values():
                if isinstance(me, MIBEntry):
                    setattr(me, MIBEntry.PREFIXLEN, _prefix_len + len(me.subtree))
                    setattr(me, MIBEntry.PREFIX, _prefix + me.subtree)

            sub_ids = {}

            # gather all MIB entries.
            mib_entries = (v for v in vars(cls).values() if isinstance(v, MIBEntry))
            for me in mib_entries:
                sub_ids.update({_prefix + me.subtree: me})
                prefixes.append(_prefix + me.subtree)

            # gather all updater instances
            updaters = set(v for k, v in vars(cls).items() if isinstance(v, MIBUpdater))

        else:
            # wrapper classes should omit the prefix.
            sub_ids = {}
            updaters = set()
            prefixes = []

        for base_cls in bases:
            # Gather any inherited MIBs
            sub_ids.update(getattr(base_cls, MIBMeta.KEYSTORE, {}))
            # Python multiple-inheritance is processed right-to-left.
            # "Pushing" to the front of the subtree list ensures that the "priority"
            # is ordered left-to-right.
            prefixes = getattr(base_cls, MIBMeta.PREFIXES, []) + prefixes
            updaters |= getattr(base_cls, MIBMeta.UPDATERS, set())

        # attach the MIB mappings
        setattr(cls, MIBMeta.KEYSTORE, sub_ids)
        setattr(cls, MIBMeta.PREFIXES, prefixes)
        setattr(cls, MIBMeta.UPDATERS, updaters)
        # class construction complete.
        return cls

    def __init__(cls, name, bases, attributes, prefix=None):
        # type only expects three arguments, '__init__' must be implemented to "pop" 'prefix'.
        type.__init__(cls, name, bases, attributes)


class MIBEntry:
    PREFIXLEN = '__prefixlen__'
    PREFIX = '__prefix__'

    def __init__(self, subtree, value_type, callable_, *args):
        """
        MIB Entry namespace container. Associates a particular OID subtree to a ValueType return and a callable
        object that provides the given information. Optionally, a persistent updater may be specified if the
        data objects require caching.

        :param subtree:
        :param value_type:
        :param callable_:
        :param args:
        :param updater:
        """
        if not util.is_valid_oid(subtree):
            raise ValueError("Invalid sub identifier: '{}'".format(subtree))
        if type(value_type) is not ValueType:
            raise ValueError("Second argument expected 'ValueType'")
        if not callable(callable_):
            raise ValueError("Third argument must be a callable object--got literal instead.")
        self._callable_ = callable_
        self._callable_args = args
        self.subtree_str = subtree
        self.value_type = value_type
        self.subtree = util.oid2tuple(subtree, dot_prefix=False)

    def __iter__(self):
        yield ()

    def __call__(self, sub_id=None):
        return self._callable_.__call__(*self._callable_args)

    def get_sub_id(self, oid_key):
        return oid_key[getattr(self, MIBEntry.PREFIXLEN) :]

    def replace_sub_id(self, oid_key, sub_id):
        return oid_key[: getattr(self, MIBEntry.PREFIXLEN)] + sub_id

    def get_next(self, sub_id):
        return None

    def get_prefix(self):
        return getattr(self, MIBEntry.PREFIX)


class SubtreeMIBEntry(MIBEntry):
    def __init__(self, subtree, iterator, value_type, callable_, *args):
        super().__init__(subtree, value_type, callable_, *args)
        self.iterator = iterator

    def __iter__(self):
        sub_id = ()
        while True:
            try:
                sub_id = self.iterator.get_next(sub_id)
            except Exception:
                # Any unexpected exception or error, log it and keep running
                logger.exception("SubtreeMIBEntry.__iter__() caught an unexpected exception during iterator.get_next()")
                break
            if sub_id is None:
                break
            yield sub_id

    def __call__(self, sub_id):
        assert isinstance(sub_id, tuple)
        try:
            return self._callable_.__call__(sub_id, *self._callable_args)
        except Exception:
            # Any unexpected exception or error, log it and keep running
            logger.exception("SubtreeMIBEntry.__call__() caught an unexpected exception during _callable_.__call__()")
            return None

    def get_next(self, sub_id):
        try:
            return self.iterator.get_next(sub_id)
        except Exception:
            # Any unexpected exception or error, log it and keep running
            logger.exception("SubtreeMIBEntry.get_next() caught an unexpected exception during iterator.get_next()")
            return None


# Define MIB entry (subtree) with a callable, which accepts a starndard OID tuple as a paramter
class OidMIBEntry(MIBEntry):
    def __init__(self, subtree, value_type, callable_):
        super().__init__(subtree, value_type, callable_)

    def __iter__(self):
        raise NotImplementedError

    def __call__(self, sub_id):
        return self._callable_.__call__(self.get_prefix() + sub_id)


class OverlayAdpaterMIBEntry(MIBEntry):
    def __init__(self, underlay_mibentry, overlay_mibentry):
        assert underlay_mibentry.value_type == overlay_mibentry.value_type
        assert underlay_mibentry.subtree == overlay_mibentry.subtree

        super().__init__(
            underlay_mibentry.subtree_str,
            underlay_mibentry.value_type,
            underlay_mibentry._callable_,
        )
        self.underlay_mibentry = underlay_mibentry
        self.overlay_mibentry = overlay_mibentry

    def __setattr__(self, name, value):
        super().__setattr__(name, value)
        if name.startswith('__') and name.endswith('__'):
            setattr(self.underlay_mibentry, name, value)
            setattr(self.overlay_mibentry, name, value)

    def __iter__(self):
        return self.underlay_mibentry.__iter__()

    def __call__(self, sub_id=None):
        overlay_val = self.overlay_mibentry(sub_id)
        if overlay_val is not None:
            return overlay_val

        underlay_val = self.underlay_mibentry(sub_id)
        return underlay_val

    def get_next(self, sub_id):
        return self.underlay_mibentry.get_next(sub_id)


class MIBTable(dict):
    """
    Simplistic LUT for Get/GetNext OID. Interprets iterables as keys and implements the same interfaces as dict's.
    """

    def __init__(self, mib_cls,
                 enable_dynamic_frequency=DEFAULT_ENABLE_DYNAMIC_FREQUENCY,
                 update_frequency=DEFAULT_UPDATE_FREQUENCY):
        if type(mib_cls) is not MIBMeta:
            raise ValueError("Supplied object is not a MIB class instance.")
        super().__init__(getattr(mib_cls, MIBMeta.KEYSTORE))
        self.enable_dynamic_frequency = enable_dynamic_frequency
        self.update_frequency = update_frequency
        self.updater_instances = getattr(mib_cls, MIBMeta.UPDATERS)
        self.prefixes = getattr(mib_cls, MIBMeta.PREFIXES)

    @staticmethod
    def _done_background_task_callback(fut):
        ex = fut.exception()
        if ex is not None:
            exstr = "MIBTable background task caught an unexpected exception: {}".format(
                str(ex)
            )
            logger.error(exstr)

    def start_background_tasks(self, event):
        tasks = []
        for updater in self.updater_instances:
            updater.frequency = self.update_frequency
            updater.enable_dynamic_frequency = self.enable_dynamic_frequency
            updater.run_event = event
            fut = asyncio.ensure_future(updater.start())
            fut.add_done_callback(MIBTable._done_background_task_callback)
            tasks.append(fut)
        return asyncio.gather(*tasks)

    def _find_parent_prefix(self, item):
        oids = sorted(self.prefixes)
        left_insert_index = bisect.bisect(oids, item)
        preceding_oids = oids[:left_insert_index]
        if not preceding_oids:
            return None
        if preceding_oids[-1] == item[: len(preceding_oids[-1])]:
            return preceding_oids[-1]
        else:
            return None

    def _get_value(self, mib_entry, oid_key):
        sub_id = mib_entry.get_sub_id(oid_key)
        oid_value = mib_entry(sub_id)
        if oid_value is None:
            return None
        # OID found, call the OIDEntry
        vr = ValueRepresentation.from_typecast(mib_entry.value_type, oid_key, oid_value)
        return vr

    def _get_nextvalue(self, mib_entry, oid_key):
        sub_id = mib_entry.get_sub_id(oid_key)
        key1 = mib_entry.get_next(sub_id)
        if key1 is None:
            return None
        val1 = mib_entry(key1)
        if val1 is None:
            return None
        oid1 = mib_entry.replace_sub_id(oid_key, key1)
        # OID found, call the OIDEntry
        vr = ValueRepresentation.from_typecast(mib_entry.value_type, oid1, val1)
        return vr

    def get(self, sr, d=None):
        oid_key = sr.start.to_tuple()

        # find the best match prefix, either a exact match or a parent prefix
        prefix = self._find_parent_prefix(oid_key)
        if prefix is not None:
            parent_mib_entry = super().get(prefix)
            vr = self._get_value(parent_mib_entry, oid_key)
            if vr is not None:
                return vr
            # we found a prefix. E.g. (1,2,3) is a prefix to OID (1,2,3,1)
            value_type = ValueType.NO_SUCH_INSTANCE
        else:
            # couldn't find the exact OID, or a valid prefix
            value_type = ValueType.NO_SUCH_OBJECT

        vr = ValueRepresentation(
            value_type,
            0,  # reserved
            sr.start,
            None,  # null value
        )
        return vr

    def get_next(self, sr):
        start_key = sr.start.to_tuple()
        end_key = sr.end.to_tuple()
        oid_list = sorted(self.prefixes)

        # find the best match prefix, either a exact match or a parent prefix
        prefix = self._find_parent_prefix(start_key)
        if prefix is not None:
            parent_mib_entry = super().get(prefix)

            if sr.start.include:
                vr = self._get_value(parent_mib_entry, start_key)
                if vr is not None:
                    return vr

            vr = self._get_nextvalue(parent_mib_entry, start_key)
            if vr is not None:
                return vr

        # return the index of an insertion point immediately following any duplicate value (thereby excluding it)
        sorted_start_index = bisect.bisect_right(oid_list, start_key)

        # slice our MIB by the insertion point.
        remaining_oids = oid_list[sorted_start_index:]

        while remaining_oids and remaining_oids[0] < end_key:
            # we found at least one remaining oid and the first entry in the remaining oid list
            # is less than our end value--it's a match.
            oid_key = remaining_oids[0]
            mib_entry = self[oid_key]
            try:
                key1 = next(iter(mib_entry))  # get the first sub_id from the mib_etnry
            except StopIteration:
                # handler returned None, which implies there's no data, keep walking.
                remaining_oids = remaining_oids[1:]
                continue

            val1 = mib_entry(key1)
            if val1 is None:
                logger.error('MIBTable.get_next found an invalid key: {}+{}'.format(mib_entry.subtree, key1))
                remaining_oids = remaining_oids[1:]
                continue

            oid1 = mib_entry.replace_sub_id(oid_key, key1)

            # found a concrete OID value--return it.
            vr = ValueRepresentation.from_typecast(
                mib_entry.value_type,
                oid1,
                val1
            )
            return vr

        # exhausted all remaining OID options--we're at the end of the MIB view.
        return ValueRepresentation(
            ValueType.END_OF_MIB_VIEW,
            0,  # reserved
            sr.start,
            None,  # null value
        )

    def __setitem__(self, key, value):
        if not hasattr(value, '__iter__'):
            raise ValueError("Invalid key '{}'. All keys must be iterable types.".format(key))
        super().__setitem__(key, value)

    def __eq__(self, other):
        if not isinstance(other, MIBTable):
            return False
        return (
            dict.__eq__(self, other)
            and self.prefixes == other.prefixes
            and self.update_frequency == other.update_frequency
            and self.updater_instances == other.updater_instances
        )
